Skip to content

Conversation

@ENvironmentSet
Copy link
Collaborator

@ENvironmentSet ENvironmentSet commented Jul 31, 2025

  • A hook usePrepare() which returns prepare(activityName[, activityParams]) is added for navigation warmup.
    • prepare(activityName[, activityParams]): Direct stackflow to prepare to navigate to given activity. This makes stackflow preload code of given activity's component if it is lazy loaded one. This also makes stackflow to preload any data fetched by pre-defined loader in stackflow config for that activity if and only if activity params are given.
    • Preloaded loader data will be cached for 30 seconds by default, however, you can set per-data-loader cache max age by defining a loader function using loader(loaderFn[, config]) while suppling loaderCacheMaxAge option.
  • A hook useActivityPreparation(activities) for preparing navigations inside a component is added.

@changeset-bot
Copy link

changeset-bot bot commented Jul 31, 2025

🦋 Changeset detected

Latest commit: 55f23c8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@stackflow/react Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai
Copy link

coderabbitai bot commented Jul 31, 2025

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added a prefetch capability to prepare activities and their data before navigation.
    • Introduced hooks to warm up single or multiple navigations.
    • Enabled lazy component preloading for smoother transitions.
    • Implemented loader data caching with configurable expiration.
  • Performance

    • Faster, more seamless navigation through preloading and cached loader data.
    • Reduced loading flashes by preparing components and data in advance.
  • Refactor

    • Internal rendering now consumes activity components and data via providers for cleaner integration.
  • Chores

    • Added a changeset marking a minor release for the React integration.

Walkthrough

Adds prefetch hooks (usePrepare, useActivityPreparation), DataLoaderContext with per-activity cached loadData and TTL, ActivityComponentMapProvider for component lookup, updates loaderPlugin/createBeforeRouteHandler to use injected loadData and await loader/component promises, and refactors render wiring to use the new contexts.

Changes

Cohort / File(s) Summary
Release metadata
**.changeset/slick-heads-tan.md**
Adds changeset documenting a minor release and the new prefetch APIs.
Activity component map context & consumers
**integrations/react/src/__internal__/ActivityComponentMapProvider.tsx**, **integrations/react/src/__internal__/MainRenderer.tsx**, **integrations/react/src/__internal__/PluginRenderer.tsx**, **integrations/react/src/stable/stackflow.tsx**
Introduces ActivityComponentMapProvider and useActivityComponentMap; removes activityComponentMap prop from MainRenderer/PluginRenderer and sources the map from context; stable stackflow wraps renderer with the provider.
Data loader context & wiring
**integrations/react/src/future/loader/DataLoaderContext.tsx**, **integrations/react/src/future/loader/index.ts**, **integrations/react/src/future/stackflow.tsx**
Adds DataLoaderContext/provider/hook; stackflow implements loadData with per-activity caching and TTL, exposes loadData via DataLoaderProvider and passes it into loaderPlugin.
Loader plugin & route handling
**integrations/react/src/future/loader/loaderPlugin.tsx**
loaderPlugin and createBeforeRouteHandler now accept injected loadData, centralize loader retrieval through loadData, detect Promise-like results, await lazy component and loader promises, and report promise errors.
Prefetch API
**integrations/react/src/future/usePrepare.ts**, **integrations/react/src/future/useActivityPreparation.ts**, **integrations/react/src/future/index.ts**
Adds usePrepare (prefetch component + loader) and useActivityPreparation (batch prepare); updates future exports to include new hooks and removes one internal re-export.
Lazy loading behavior
**integrations/react/src/future/lazy.ts**
Caches the Promise returned by load(), resets cache on rejection to allow retries; public lazy API unchanged.
Utilities
**integrations/react/src/__internal__/utils/isPromiseLike.ts**
Adds isPromiseLike type-guard for detecting Promise-like values.
Config: ActivityLoader type & helper
**config/src/ActivityLoader.ts**
Changes ActivityLoader to a callable object with optional loaderCacheMaxAge and adds helper loader() to construct such loaders.
Minor refactors & formatting
**integrations/react/src/future/makeActions.ts**, **integrations/react/src/future/useFlow.ts**, **integrations/react/src/__internal__/MainRenderer.tsx**
Small refactors (replace activityId evaluation, blank-line insertion, prop removals); no additional public API changes beyond items above.

Sequence Diagram(s)

sequenceDiagram
  participant UI as Caller
  participant Prep as usePrepare
  participant ACM as ActivityComponentMapProvider
  participant DL as loadData (stackflow)
  participant ACT as Activity loader / component

  UI->>Prep: prepare(activityName, params?)
  Prep->>ACM: get component entry (check _load)
  alt has _load
    Prep->>ACT: call _load() (lazy component)
  end
  alt params & loader exist
    Prep->>DL: loadData(activityName, params)
    alt cache hit
      DL-->>Prep: cached loaderData
    else
      DL->>ACT: invoke activity.loader(params)
      ACT-->>DL: loaderData (cached + TTL)
      DL-->>Prep: loaderData
    end
  end
  Prep-->>UI: resolves
Loading
sequenceDiagram
  participant User as User
  participant Actions as Actions
  participant Plugins as Plugins
  participant LP as loaderPlugin
  participant BR as beforeRouteHandler
  participant DL as loadData

  User->>Actions: push/replace(...)
  Actions->>Plugins: dispatch
  Plugins->>LP: onBeforePush/Replace
  LP->>BR: createBeforeRouteHandler(input, loadData)
  BR->>DL: loadData(activityName, params?) (if loader)
  BR->>LP: await lazy component + loader promises
  BR-->>LP: continue with actionParams (loaderData attached)
  LP-->>Plugins: event updated
  Plugins-->>Actions: proceed
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch route-preload

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Jul 31, 2025

Deploying stackflow-demo with  Cloudflare Pages  Cloudflare Pages

Latest commit: 55f23c8
Status: ✅  Deploy successful!
Preview URL: https://7a85fd26.stackflow-demo.pages.dev
Branch Preview URL: https://route-preload.stackflow-demo.pages.dev

View logs

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jul 31, 2025

@stackflow/demo

yarn add https://pkg.pr.new/@stackflow/[email protected]
yarn add https://pkg.pr.new/@stackflow/[email protected]

commit: 55f23c8

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Jul 31, 2025

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
stackflow-docs 55f23c8 Commit Preview URL Aug 18 2025, 09:40 AM

@ENvironmentSet ENvironmentSet changed the title feat(future, link): Add prefetch API for lazy activity component and loader data. feat(future): Add prefetch API for lazy activity component and loader data. Aug 10, 2025
@ENvironmentSet ENvironmentSet marked this pull request as ready for review August 10, 2025 12:12
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🔭 Outside diff range comments (1)
integrations/react/src/future/loader/loaderPlugin.tsx (1)

109-116: Reuse thenable detection for onBeforePush/onBeforeReplace

Consistently handle thenables here as well.

-    const loaderDataPromise =
-      loaderData instanceof Promise ? loaderData : undefined;
+    const loaderDataPromise =
+      loaderData && typeof (loaderData as any).then === "function"
+        ? (loaderData as Promise<unknown>)
+        : undefined;
🧹 Nitpick comments (8)
.changeset/slick-heads-tan.md (1)

1-6: Clarify API names and options in the changeset

Suggest explicitly listing:

  • New action: actions.prepare(activityName[, activityParams])
  • New hook: useActivityPreparation(activityNames)
  • New option: stackflow({ options: { loaderCacheMaxAge } })
  • One-liner on cache semantics and lazy component prefetch.

Improves discoverability for consumers scanning release notes.

integrations/react/src/future/useActivityPreparation.ts (1)

4-4: Consider supporting params to prefetch loader data

Hook currently only preloads components. If loader prefetch requires params, consider an overload or accepting tuples like { name, params }.

Please confirm whether prepare(activityName) without params triggers loader prefetch; if not, the hook name/docs should clarify it only preloads components.

integrations/react/src/__internal__/PluginRenderer.tsx (1)

35-39: Optional: Guard for missing component mapping

Defensive check helps fail fast with a clear message if a mapping is missing at runtime.

-              const Activity = activityComponentMap[activity.name];
+              const Activity = activityComponentMap[activity.name];
+              if (!Activity) {
+                throw new Error(
+                  `No component registered for activity "${activity.name}"`,
+                );
+              }
integrations/react/src/future/lazy.ts (1)

8-12: Nit: Name reflects content (promise vs value)

cachedValue now holds a Promise; consider renaming to cachedPromise for clarity. No functional change.

integrations/react/src/future/stackflow.tsx (1)

102-105: Use referential equality for eviction

Deep comparing whole entries (including data) is unnecessary and slower. The exact object reference can be removed safely.

-        cache.filter((entry) => !isEqual(entry, newCacheEntry)),
+        cache.filter((entry) => entry !== newCacheEntry),
integrations/react/src/future/makeActions.ts (2)

33-33: Reflect async loader in the type of loadData

Actions.prepare wraps loader results in Promise.resolve. Make the type explicit for better DX.

-  loadData: (activityName: string, activityParams: {}) => unknown,
+  loadData: (activityName: string, activityParams: {}) => unknown | Promise<unknown>,

110-114: Narrow to LazyActivityComponentType before calling _load for stronger type safety

This avoids property access on a broad component type and prevents TS errors under stricter configs.

-      if ("_load" in activityComponentMap[activityName]) {
-        const lazyComponent = activityComponentMap[activityName];
-
-        prefetchTasks.push(Promise.resolve(lazyComponent._load?.()));
-      }
+      if ("_load" in activityComponentMap[activityName]) {
+        const lazyComponent =
+          activityComponentMap[activityName] as LazyActivityComponentType<any>;
+        prefetchTasks.push(Promise.resolve(lazyComponent._load?.()));
+      }
integrations/react/src/future/loader/loaderPlugin.tsx (1)

109-110: Minor: skip calling loadData when the activity has no loader

Avoids unnecessary cache entries/timers when no loader exists.

-    const loaderData = loadData(activityName, activityParams);
+    const loaderData = matchActivity.loader
+      ? loadData(activityName, activityParams)
+      : undefined;
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 658c770 and c61676b.

📒 Files selected for processing (14)
  • .changeset/slick-heads-tan.md (1 hunks)
  • integrations/react/src/__internal__/MainRenderer.tsx (1 hunks)
  • integrations/react/src/__internal__/PluginRenderer.tsx (1 hunks)
  • integrations/react/src/future/Actions.ts (1 hunks)
  • integrations/react/src/future/ActivityComponentMapProvider.tsx (1 hunks)
  • integrations/react/src/future/index.ts (1 hunks)
  • integrations/react/src/future/lazy.ts (1 hunks)
  • integrations/react/src/future/loader/DataLoaderContext.tsx (1 hunks)
  • integrations/react/src/future/loader/index.ts (1 hunks)
  • integrations/react/src/future/loader/loaderPlugin.tsx (3 hunks)
  • integrations/react/src/future/makeActions.ts (3 hunks)
  • integrations/react/src/future/stackflow.tsx (7 hunks)
  • integrations/react/src/future/useActivityPreparation.ts (1 hunks)
  • integrations/react/src/future/useFlow.ts (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Workers Builds: stackflow-docs
🔇 Additional comments (5)
integrations/react/src/future/index.ts (1)

25-25: Public API: re-export useActivityPreparation — LGTM

Consistent with existing hook exports. No concerns.

integrations/react/src/future/loader/index.ts (1)

3-3: Barrel export for DataLoaderContext — LGTM

Keeps loader API cohesive. No issues spotted.

integrations/react/src/__internal__/PluginRenderer.tsx (2)

2-2: LGTM on switching to context-based activityComponentMap

Importing useActivityComponentMap aligns with the new provider pattern.


17-17: PluginRenderer is always wrapped by ActivityComponentMapProvider

Verified that:

  • PluginRenderer (integrations/react/src/internal/PluginRenderer.tsx) is only rendered by MainRenderer.
  • MainRenderer (integrations/react/src/internal/MainRenderer.tsx) is only used in future/stackflow.tsx.
  • In integrations/react/src/future/stackflow.tsx (lines 221–228), ActivityComponentMapProvider fully encloses MainRenderer (and thus every PluginRenderer instance).

No further action required.

integrations/react/src/future/ActivityComponentMapProvider.tsx (1)

1-38: Solid provider/hook; error message is clear

Context shape and guard are good. No issues.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🔭 Outside diff range comments (3)
integrations/react/src/__internal__/MainRenderer.tsx (1)

2-2: Remove obsolete activityComponentMap prop and unused import

The component no longer consumes activityComponentMap. Clean up the prop type and import to avoid confusion and unused types.

-import type { ActivityComponentType } from "./ActivityComponentType";
@@
-interface MainRendererProps {
-  activityComponentMap: {
-    [key: string]: ActivityComponentType;
-  };
-  initialContext: any;
-}
+interface MainRendererProps {
+  initialContext: any;
+}

Also applies to: 9-15

integrations/react/src/future/makeActions.ts (1)

50-63: replace() returns an ID that may not match the one used in the core action

You compute activityId but call replace() with a different ID. This breaks consumer expectations and tracking.

-    replace(activityName, activityParams, options) {
-      const activityId = makeActivityId();
+    replace(activityName, activityParams, options) {
+      const activityId = options?.activityId ?? makeActivityId();
@@
-      getCoreActions()?.replace({
-        activityId: options?.activityId ?? makeActivityId(),
+      getCoreActions()?.replace({
+        activityId,
         activityName,
         activityParams,
         skipEnterActiveState: parseActionOptions(options).skipActiveState,
       });
integrations/react/src/future/loader/loaderPlugin.tsx (1)

90-113: Avoid unnecessary loadData calls and handle sync throws in route handler

Only call loadData if a loader exists, and normalize sync throws to a rejected promise for consistent pause/resume behavior.

-  input: StackflowInput<T, R>,
-  loadData: (activityName: string, activityParams: {}) => unknown,
+  input: StackflowInput<T, R>,
+  loadData: (activityName: string, activityParams: {}) => unknown,
 ): OnBeforeRoute {
@@
-    const loaderData = loadData(activityName, activityParams);
-
-    const loaderDataPromise =
-      loaderData instanceof Promise ? loaderData : undefined;
+    let loaderData: unknown | undefined;
+    if (matchActivity.loader) {
+      try {
+        loaderData = loadData(activityName, activityParams);
+      } catch (err) {
+        loaderData = Promise.reject(err);
+      }
+    }
+    const loaderDataPromise =
+      loaderData instanceof Promise ? loaderData : undefined;
🧹 Nitpick comments (6)
integrations/react/src/future/useFlow.ts (1)

14-22: Stabilize Actions object with useMemo

makeActions(...) is executed on every render, changing the Actions object identity and potentially causing unnecessary re-renders. Memoize based on inputs.

+import { useMemo } from "react";
@@
-  const actions = makeActions(
-    config,
-    () => coreActions,
-    activityComponentMap,
-    loadData,
-  );
-
-  return actions;
+  const actions = useMemo(
+    () =>
+      makeActions(
+        config,
+        () => coreActions,
+        activityComponentMap,
+        loadData,
+      ),
+    [config, coreActions, activityComponentMap, loadData],
+  );
+
+  return actions;
integrations/react/src/future/ActivityComponentMapProvider.tsx (1)

1-38: Solid context API; minor type-hardening optional

Implementation looks correct and aligns with the new access pattern. Optionally mark the map as readonly to prevent accidental mutation.

-const ActivityComponentMapContext = createContext<
-  | {
-      [activityName in RegisteredActivityName]: ActivityComponentType;
-    }
-  | null
->(null);
+const ActivityComponentMapContext = createContext<
+  | Readonly<Record<RegisteredActivityName, ActivityComponentType>>
+  | null
+>(null);
@@
-type ActivityComponentMapProviderProps = PropsWithChildren<{
-  value: {
-    [activityName in RegisteredActivityName]: ActivityComponentType;
-  };
-}>;
+type ActivityComponentMapProviderProps = PropsWithChildren<{
+  value: Readonly<Record<RegisteredActivityName, ActivityComponentType>>;
+}>;
integrations/react/src/__internal__/MainRenderer.tsx (1)

15-15: Optional: Narrow any

If feasible, narrow initialContext: any to a more specific type or unknown to reduce implicit-any usage.

-const MainRenderer: React.FC<MainRendererProps> = ({ initialContext }) => {
+const MainRenderer: React.FC<MainRendererProps> = ({ initialContext }) => {

(Consider updating MainRendererProps.initialContext to unknown or a concrete type.)

integrations/react/src/future/loader/DataLoaderContext.tsx (1)

3-5: Unify and reuse the loader function type via a named alias

Define a DataLoaderFn type and reuse it here to reduce duplication and keep signatures consistent across modules.

Apply:

 export const DataLoaderContext = createContext<
-  ((activityName: string, activityParams: {}) => unknown) | null
+  DataLoaderFn | null
 >(null);
@@
 }: {
-  loadData: (activityName: string, activityParams: {}) => unknown;
+  loadData: DataLoaderFn;
   children: ReactNode;
 })

Add near the top (outside the selected range):

export type DataLoaderFn = (activityName: string, activityParams: {}) => unknown;

Also applies to: 11-13

integrations/react/src/future/makeActions.ts (1)

110-114: Type-narrow lazy components before calling _load

Use a type guard (or cast) so TS understands _load exists on lazy components.

-      if ("_load" in activityComponentMap[activityName]) {
-        const lazyComponent = activityComponentMap[activityName];
-
-        prefetchTasks.push(Promise.resolve(lazyComponent._load?.()));
-      }
+      if ("_load" in activityComponentMap[activityName]) {
+        const lazyComponent =
+          activityComponentMap[activityName] as LazyActivityComponentType<any>;
+        prefetchTasks.push(Promise.resolve(lazyComponent._load?.()));
+      }

If preferred, add a type guard outside the selected range and use it:

function isLazyActivityComponent(
  c: ActivityComponentType<any>,
): c is LazyActivityComponentType<any> {
  return typeof (c as any)._load === "function";
}
integrations/react/src/future/stackflow.tsx (1)

70-73: Consider refreshing TTL on cache hits (sliding expiration)

Returning a cached value does not extend its lifetime. If desired UX is “keep warm while accessed,” refresh expiry when a hit occurs.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 658c770 and c61676b.

📒 Files selected for processing (14)
  • .changeset/slick-heads-tan.md (1 hunks)
  • integrations/react/src/__internal__/MainRenderer.tsx (1 hunks)
  • integrations/react/src/__internal__/PluginRenderer.tsx (1 hunks)
  • integrations/react/src/future/Actions.ts (1 hunks)
  • integrations/react/src/future/ActivityComponentMapProvider.tsx (1 hunks)
  • integrations/react/src/future/index.ts (1 hunks)
  • integrations/react/src/future/lazy.ts (1 hunks)
  • integrations/react/src/future/loader/DataLoaderContext.tsx (1 hunks)
  • integrations/react/src/future/loader/index.ts (1 hunks)
  • integrations/react/src/future/loader/loaderPlugin.tsx (3 hunks)
  • integrations/react/src/future/makeActions.ts (3 hunks)
  • integrations/react/src/future/stackflow.tsx (7 hunks)
  • integrations/react/src/future/useActivityPreparation.ts (1 hunks)
  • integrations/react/src/future/useFlow.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (6)
integrations/react/src/future/useActivityPreparation.ts (2)
config/src/RegisteredActivityName.ts (1)
  • RegisteredActivityName (3-6)
integrations/react/src/future/useFlow.ts (1)
  • useFlow (12-25)
integrations/react/src/future/Actions.ts (2)
config/src/RegisteredActivityName.ts (1)
  • RegisteredActivityName (3-6)
config/src/InferActivityParams.ts (1)
  • InferActivityParams (5-8)
integrations/react/src/future/lazy.ts (1)
integrations/react/src/__internal__/StaticActivityComponentType.ts (1)
  • StaticActivityComponentType (3-5)
integrations/react/src/future/ActivityComponentMapProvider.tsx (1)
config/src/RegisteredActivityName.ts (1)
  • RegisteredActivityName (3-6)
integrations/react/src/future/makeActions.ts (5)
config/src/Config.ts (1)
  • Config (5-10)
config/src/ActivityDefinition.ts (1)
  • ActivityDefinition (4-9)
config/src/RegisteredActivityName.ts (1)
  • RegisteredActivityName (3-6)
core/src/makeCoreStore.ts (1)
  • CoreStore (30-36)
config/src/InferActivityParams.ts (1)
  • InferActivityParams (5-8)
integrations/react/src/future/stackflow.tsx (6)
config/src/ActivityDefinition.ts (1)
  • ActivityDefinition (4-9)
config/src/RegisteredActivityName.ts (1)
  • RegisteredActivityName (3-6)
integrations/react/src/future/loader/loaderPlugin.tsx (1)
  • loaderPlugin (9-79)
integrations/react/src/future/ActivityComponentMapProvider.tsx (1)
  • ActivityComponentMapProvider (29-38)
integrations/react/src/future/loader/DataLoaderContext.tsx (1)
  • DataLoaderProvider (7-19)
integrations/react/src/future/makeActions.ts (1)
  • makeActions (27-119)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Workers Builds: stackflow-docs
🔇 Additional comments (7)
integrations/react/src/future/index.ts (1)

25-25: LGTM! Export addition is correctly placed.

The new useActivityPreparation hook is properly exported in the hooks section, following the existing pattern and making it available to consumers of the public API.

integrations/react/src/future/loader/index.ts (1)

3-3: LGTM! DataLoaderContext export is properly added.

The export correctly exposes the DataLoaderContext which is essential for the prefetch functionality's centralized data loading approach.

.changeset/slick-heads-tan.md (1)

1-6: LGTM! Changeset properly documents the feature addition.

The changeset correctly specifies a minor version bump for @stackflow/react and accurately describes the prefetch API feature being added.

integrations/react/src/future/Actions.ts (1)

29-32: LGTM! Well-designed prepare method signature.

The new prepare method follows the established patterns:

  • Uses the same generic constraint as other methods for type safety
  • Optional activityParams provides flexibility for different prefetch scenarios
  • Promise<void> return type is appropriate for async prefetch operations
integrations/react/src/__internal__/PluginRenderer.tsx (1)

2-2: Good move: eliminate prop drilling for activityComponentMap

Switching to useActivityComponentMap() simplifies wiring and aligns with the new provider-based architecture.

Also applies to: 17-17

integrations/react/src/__internal__/MainRenderer.tsx (1)

15-15: ActivityComponentMapProvider correctly wraps MainRenderer

Confirmed that in integrations/react/src/future/stackflow.tsx the <ActivityComponentMapProvider> (lines 221–228) wraps <MainRenderer>, ensuring useActivityComponentMap has the required context. No further changes needed here.

integrations/react/src/future/stackflow.tsx (1)

223-226: Avoid passing activityComponentMap prop if now provided via context

You’re providing the map via ActivityComponentMapProvider and also passing activityComponentMap to MainRenderer. Drop the prop to prevent API duplication/drift.

-                <MainRenderer
-                  initialContext={initialContext}
-                  activityComponentMap={input.components}
-                />
+                <MainRenderer initialContext={initialContext} />

@daangn daangn deleted a comment from opencode-agent bot Aug 11, 2025
@daangn daangn deleted a comment from opencode-agent bot Aug 11, 2025
@daangn daangn deleted a comment from opencode-agent bot Aug 11, 2025
@daangn daangn deleted a comment from opencode-agent bot Aug 11, 2025
@orionmiz
Copy link
Collaborator

/oc Run git rebase origin/main and resolve all merge conflicts while rebasing, and push it.

@opencode-agent
Copy link

opencode-agent bot commented Aug 11, 2025

Rebase completed successfully!

I successfully:

  1. Ran git rebase origin/main - Rebased the route-preload branch onto the latest main
  2. Resolved 3 merge conflicts:
    • PluginRenderer.tsx - Fixed import conflicts between old ActivityComponentType and new useActivityComponentMap
    • stackflow.tsx - Resolved import conflicts for DataLoaderProvider and loaderPlugin
    • index.ts - Cleaned up duplicate exports and reorganized the export structure
  3. Force pushed the changes - Used --force-with-lease to safely update the remote branch

The branch is now successfully rebased onto the latest main branch and all conflicts have been resolved.

Rebasing%20and%20resolving%20git%20conflicts
opencode session  |  github run

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
integrations/react/src/future/stackflow.tsx (1)

63-109: Don't cache when there's no loader; evict on rejected loader promises; clean up empty buckets

Currently, activities without a loader are cached with data: undefined, and rejected Promises remain cached until TTL. This causes unnecessary cache entries and prevents retries after failures within the TTL window.

   const loadData = (activityName: string, activityParams: {}) => {
@@
-    const loaderData = activityConfig.loader?.({
-      params: activityParams,
-      config: input.config,
-    });
+    if (!activityConfig.loader) {
+      // No loader for this activity: don't cache undefined
+      return undefined;
+    }
+
+    const loaderData = activityConfig.loader({
+      params: activityParams,
+      config: input.config,
+    });
     const newCacheEntry = {
       params: activityParams,
       data: loaderData,
     };
 
     if (cache) {
       cache.push(newCacheEntry);
     } else {
       loaderDataCacheMap.set(activityName, [newCacheEntry]);
     }
 
+    // If the loader rejects, evict this entry immediately to allow retrial
+    if (loaderData instanceof Promise) {
+      loaderData.catch(() => {
+        const cache = loaderDataCacheMap.get(activityName);
+        if (!cache) return;
+        const next = cache.filter((entry) => entry !== newCacheEntry);
+        if (next.length > 0) {
+          loaderDataCacheMap.set(activityName, next);
+        } else {
+          loaderDataCacheMap.delete(activityName);
+        }
+      });
+    }
+
     setTimeout(() => {
       const cache = loaderDataCacheMap.get(activityName);
 
       if (!cache) return;
 
-      loaderDataCacheMap.set(
-        activityName,
-        cache.filter((entry) => entry !== newCacheEntry),
-      );
+      const next = cache.filter((entry) => entry !== newCacheEntry);
+      if (next.length > 0) {
+        loaderDataCacheMap.set(activityName, next);
+      } else {
+        loaderDataCacheMap.delete(activityName);
+      }
     }, input.options?.loaderCacheMaxAge ?? DEFAULT_LOADER_CACHE_MAX_AGE);
🧹 Nitpick comments (4)
integrations/react/src/future/makeActions.ts (2)

5-17: Simplify parseActionOptions; reduce branches while preserving semantics

Current logic returns the same result using multiple early returns. This can be simplified and made more readable.

Apply this diff:

-function parseActionOptions(options?: { animate?: boolean }) {
-	if (!options) {
-		return { skipActiveState: false };
-	}
-
-	const isNullableAnimateOption = options.animate == null;
-
-	if (isNullableAnimateOption) {
-		return { skipActiveState: false };
-	}
-
-	return { skipActiveState: !options.animate };
-}
+function parseActionOptions(options?: { animate?: boolean }) {
+  const animate = options?.animate;
+  // Skip transitions only when animate is explicitly false; otherwise don't skip.
+  return { skipActiveState: animate === false };
+}

49-77: Avoid recomputing parseActionOptions inside the loop

Minor readability/perf nit: compute the first-skip value once outside the loop.

Apply this diff:

       if (options) {
         _options = {
           ...options,
         };
       }

-      for (let i = 0; i < _count; i += 1) {
-        getCoreActions()?.pop({
-          skipExitActiveState:
-            i === 0 ? parseActionOptions(_options).skipActiveState : true,
-        });
-      }
+      const skipFirstExit = parseActionOptions(_options).skipActiveState;
+      for (let i = 0; i < _count; i += 1) {
+        getCoreActions()?.pop({
+          skipExitActiveState: i === 0 ? skipFirstExit : true,
+        });
+      }
integrations/react/src/future/usePrepare.ts (1)

11-16: Stabilize the returned function identity with useCallback

Small ergonomics improvement: memoize the returned prepare to avoid referential changes across re-renders.

-export function usePrepare(): Prepare {
-  const config = useConfig();
-  const loadData = useDataLoader();
-  const activityComponentMap = useActivityComponentMap();
-
-  return async function prepare<K extends RegisteredActivityName>(
+export function usePrepare(): Prepare {
+  const config = useConfig();
+  const loadData = useDataLoader();
+  const activityComponentMap = useActivityComponentMap();
+
+  const prepare = useCallback(
+    async function prepare<K extends RegisteredActivityName>(

Outside this hunk, add the import:

import { useCallback } from "react";

And at the end of the function return the memoized callback:

  }, [config, loadData, activityComponentMap]);

  return prepare;
integrations/react/src/future/stackflow.tsx (1)

44-46: Configurable cache TTL is a solid choice

options.loaderCacheMaxAge adds needed flexibility. Consider documenting units (ms) in JSDoc for clarity.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dfd5291 and 2dee06d.

📒 Files selected for processing (7)
  • .changeset/slick-heads-tan.md (1 hunks)
  • integrations/react/src/future/index.ts (1 hunks)
  • integrations/react/src/future/makeActions.ts (1 hunks)
  • integrations/react/src/future/stackflow.tsx (6 hunks)
  • integrations/react/src/future/useActivityPreparation.ts (1 hunks)
  • integrations/react/src/future/useFlow.ts (1 hunks)
  • integrations/react/src/future/usePrepare.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • integrations/react/src/future/useActivityPreparation.ts
  • integrations/react/src/future/useFlow.ts
  • .changeset/slick-heads-tan.md
🧰 Additional context used
🧬 Code Graph Analysis (3)
integrations/react/src/future/usePrepare.ts (4)
config/src/RegisteredActivityName.ts (1)
  • RegisteredActivityName (3-6)
config/src/InferActivityParams.ts (1)
  • InferActivityParams (5-8)
integrations/react/src/future/loader/DataLoaderContext.tsx (1)
  • useDataLoader (21-29)
integrations/react/src/__internal__/ActivityComponentMapProvider.tsx (1)
  • useActivityComponentMap (13-21)
integrations/react/src/future/makeActions.ts (3)
core/src/makeCoreStore.ts (1)
  • CoreStore (29-35)
integrations/react/src/future/Actions.ts (1)
  • Actions (6-29)
integrations/react/src/__internal__/activity/makeActivityId.ts (1)
  • makeActivityId (3-5)
integrations/react/src/future/stackflow.tsx (7)
integrations/react/src/stable/stackflow.tsx (1)
  • stackflow (130-401)
config/src/ActivityDefinition.ts (1)
  • ActivityDefinition (4-9)
config/src/RegisteredActivityName.ts (1)
  • RegisteredActivityName (3-6)
integrations/react/src/__internal__/ActivityComponentType.ts (1)
  • ActivityComponentType (4-6)
integrations/react/src/future/loader/loaderPlugin.tsx (1)
  • loaderPlugin (10-80)
integrations/react/src/__internal__/ActivityComponentMapProvider.tsx (1)
  • ActivityComponentMapProvider (29-38)
integrations/react/src/future/loader/DataLoaderContext.tsx (1)
  • DataLoaderProvider (7-19)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build all packages
  • GitHub Check: Create PR or release packages
  • GitHub Check: Check whether the written test passes normally
  • GitHub Check: Check the TypeScript typings
  • GitHub Check: Workers Builds: stackflow-docs
🔇 Additional comments (8)
integrations/react/src/future/makeActions.ts (3)

21-34: push implementation LGTM

Generates a single activityId, forwards animate semantics correctly, and returns the id. Clean and consistent.


35-48: replace implementation LGTM

Using a local activityId (preferring provided one) improves readability and avoids duplicated expressions. Animate handling matches push.


19-21: makeActions aligns with the current Actions type

  • Checked integrations/react/src/future/Actions.ts: the exported Actions type only declares push, replace, and pop.
  • Confirmed integrations/react/src/future/makeActions.ts implements exactly those three methods.
  • There is no prepare on the Actions type, so no missing implementation or type/runtime gap today.

If you later extend Actions to include a prepare method, remember to update makeActions to return it as well.

integrations/react/src/future/index.ts (1)

12-13: Public API: new exports look correct; ensure they’re included in the published bundle

Exposing useActivityPreparation and usePrepare from the future barrel aligns with the new prefetch API. Confirm rollup/tsup configs include these files so they ship in the package entrypoints.

integrations/react/src/future/usePrepare.ts (1)

16-27: Good validation of unknown activities

Throwing for unknown activities prevents silent no-ops and surfaces config mistakes early.

integrations/react/src/future/stackflow.tsx (3)

14-16: Context introduction LGTM

Importing react-fast-compare and ActivityComponentMapProvider aligns with deep param cache lookups and context-driven component access.


118-118: Plugin wiring looks correct

Passing loadData into loaderPlugin coordinates centralized data resolution with the plugin hooks.


221-225: Provider composition is appropriate

Wrapping MainRenderer with ActivityComponentMapProvider and DataLoaderProvider ensures hooks like useActivityComponentMap and useDataLoader are available throughout the tree, including usePrepare.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2dee06d and 9ceb57f.

📒 Files selected for processing (1)
  • integrations/react/src/__internal__/utils/isThenable.ts (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Workers Builds: stackflow-docs
  • GitHub Check: Create PR or release packages
  • GitHub Check: Check the TypeScript typings
  • GitHub Check: Check whether the written test passes normally
  • GitHub Check: Build all packages

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
integrations/react/src/__internal__/utils/isPromiseLike.ts (2)

2-7: Broaden thenable detection to include callable thenables (Promises/A+).

Promises/A+ defines a thenable as an object or function with a then method. Current check excludes callable thenables. This is a defensive improvement and avoids rare false negatives.

Apply this diff:

 export function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
   return (
-    typeof value === "object" &&
+    (typeof value === "object" || typeof value === "function") &&
     value !== null &&
     "then" in value &&
-    typeof value.then === "function"
+    typeof (value as { then: unknown }).then === "function"
   );
 }

1-8: Add a brief JSDoc for clarity and future maintainability.

This utility will be widely used in loader handling; documenting the intended semantics (thenable detection) helps avoid misuses and regressions.

+/**
+ * Returns true if the given value is a thenable (Promise-like): an object or function
+ * with a `then` method. Useful when a loader may return either a value or a promise.
+ */
 export function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
   return (
     (typeof value === "object" || typeof value === "function") &&
     value !== null &&
     "then" in value &&
     typeof (value as { then: unknown }).then === "function"
   );
 }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9ceb57f and 85868a3.

📒 Files selected for processing (2)
  • integrations/react/src/__internal__/utils/isPromiseLike.ts (1 hunks)
  • integrations/react/src/future/loader/loaderPlugin.tsx (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • integrations/react/src/future/loader/loaderPlugin.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Workers Builds: stackflow-docs
  • GitHub Check: Check whether the written test passes normally
  • GitHub Check: Check the TypeScript typings
  • GitHub Check: Build all packages
  • GitHub Check: Create PR or release packages
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (1)
integrations/react/src/__internal__/utils/isPromiseLike.ts (1)

1-8: All thenable checks consolidated – no remaining legacy or ad-hoc “.then” checks found

I ran a repository-wide search and confirmed:

  • No references to a legacy isThenable helper.
  • No ad-hoc checks for "then" in or typeof …\.then === "function" outside of the new utility itself.

The codebase consistently uses isPromiseLike for thenable detection.
No further changes are needed here.

@ENvironmentSet ENvironmentSet changed the title feat(future): Add prefetch API for lazy activity component and loader data. feat(react/future): Add prefetch API for lazy activity component and loader data. Aug 13, 2025
@ENvironmentSet ENvironmentSet requested review from orionmiz and tonyfromundefined and removed request for orionmiz and tonyfromundefined August 14, 2025 02:26
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
integrations/react/src/future/stackflow.tsx (2)

79-86: Skip caching when an activity has no loader

Caching an entry with undefined data for activities without a loader causes unnecessary memory usage and timers. Return early instead. This mirrors earlier feedback.

-    const loaderData = activityConfig.loader?.({
-      params: activityParams,
-      config: input.config,
-    });
+    if (!activityConfig.loader) {
+      // No loader for this activity: don't cache undefined
+      return undefined;
+    }
+
+    const loaderData = activityConfig.loader({
+      params: activityParams,
+      config: input.config,
+    });

Also applies to: 88-93


99-103: Delete the map entry when the cache bucket becomes empty

Avoid retaining empty arrays in the map to reduce memory footprint and keep the structure clean. This also aligns with prior feedback to evict by reference and cleanly remove buckets.

-      loaderDataCacheMap.set(
-        activityName,
-        cache.filter((entry) => entry !== newCacheEntry),
-      );
+      const next = cache.filter((entry) => entry !== newCacheEntry);
+      if (next.length > 0) {
+        loaderDataCacheMap.set(activityName, next);
+      } else {
+        loaderDataCacheMap.delete(activityName);
+      }
🧹 Nitpick comments (2)
config/src/ActivityLoader.ts (1)

4-7: Tighten the type surface: mark TTL as readonly; consider generics over the return type

  • Marking loaderCacheMaxAge as readonly communicates intent and prevents accidental mutation elsewhere.
  • Consider making the return type generic (e.g., <TData = unknown>) instead of any to improve type-safety end-to-end.
-export type ActivityLoader<ActivityName extends RegisteredActivityName> = {
-  (args: ActivityLoaderArgs<ActivityName>): any;
-  loaderCacheMaxAge?: number;
-};
+export type ActivityLoader<ActivityName extends RegisteredActivityName, TData = unknown> = {
+  (args: ActivityLoaderArgs<ActivityName>): TData;
+  readonly loaderCacheMaxAge?: number;
+};

Note: If you adopt the generic return type here, propagate it through call sites where ActivityLoader is referenced.

integrations/react/src/future/stackflow.tsx (1)

60-62: Narrow the types for loader cache keys and params

Using RegisteredActivityName and ActivityBaseParams improves type safety across the stack and matches the config types.

-  const loaderDataCacheMap = new Map<string, { params: {}; data: unknown }[]>();
-  const loadData = (activityName: string, activityParams: {}) => {
+  const loaderDataCacheMap = new Map<
+    RegisteredActivityName,
+    { params: ActivityBaseParams; data: unknown }[]
+  >();
+  const loadData = (
+    activityName: RegisteredActivityName,
+    activityParams: ActivityBaseParams,
+  ) => {
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b155672 and 24c1600.

📒 Files selected for processing (2)
  • config/src/ActivityLoader.ts (1 hunks)
  • integrations/react/src/future/stackflow.tsx (5 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Workers Builds: stackflow-docs
  • GitHub Check: Build all packages
  • GitHub Check: Check whether the written test passes normally
  • GitHub Check: Create PR or release packages
  • GitHub Check: Check the TypeScript typings
🔇 Additional comments (2)
integrations/react/src/future/stackflow.tsx (2)

128-129: Plugin placement looks correct

Appending loaderPlugin after user-provided plugins ensures it runs after historySyncPlugin when present.


231-235: Provider composition LGTM

Supplying ActivityComponentMapProvider and DataLoaderProvider around MainRenderer matches the new architecture and reduces prop plumbing.

anakin_karrot added 2 commits August 18, 2025 18:21
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
integrations/react/src/future/useActivityPreparation.ts (1)

5-11: Optionally accept activityParams and avoid floating Promises

Since prepare supports (activityName[, activityParams]), you can pass through optional params now and explicitly discard the returned Promise to appease “no-floating-promises” linters. Also consider a readonly array to discourage mutation.

Apply:

 export function useActivityPreparation(
-  activities: { activityName: RegisteredActivityName }[],
-) {
+  activities: ReadonlyArray<{
+    activityName: RegisteredActivityName;
+    activityParams?: unknown;
+  }>,
+): void {
   const prepare = usePrepare();

-  for (const { activityName } of activities) {
-    prepare(activityName);
+  for (const { activityName, activityParams } of activities) {
+    void prepare(activityName, activityParams);
   }
 }

Notes:

  • If you prefer stronger typing for activityParams, we can introduce a mapped type later without breaking callers.
.changeset/slick-heads-tan.md (1)

5-7: Tighten wording and include cache TTL detail

Minor grammar/style fixes and add the default cache TTL + configuration knob to make the release notes more actionable.

-Add prefetch API for lazy activity component and loader data.
-- A hook `usePrepare()` which returns `prepare(activityName[, activityParams])` is added for navigation warmup.
-- A hook `useActivityPreparation(activities)` for preparing navigations inside a component is added.
+Add a prefetch API for lazy activity components and their loader data.
+- New: `usePrepare()` returns `prepare(activityName[, activityParams])` for navigation warm-up.
+- New: `useActivityPreparation(activities)` prepares navigations inside a component. Each item is `{ activityName }`.
+- Preloaded loader data is cached for 30 seconds by default; configure the max age via `options.loaderCacheMaxAge`.
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1ef17c7 and 55f23c8.

📒 Files selected for processing (2)
  • .changeset/slick-heads-tan.md (1 hunks)
  • integrations/react/src/future/useActivityPreparation.ts (1 hunks)
🧰 Additional context used
🪛 LanguageTool
.changeset/slick-heads-tan.md

[grammar] ~6-~6: There might be a mistake here.
Context: ...arams])is added for navigation warmup. - A hookuseActivityPreparation(activitie...

(QB_NEW_EN)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Check whether the written test passes normally
  • GitHub Check: Create PR or release packages
  • GitHub Check: Build all packages
  • GitHub Check: Check the TypeScript typings
  • GitHub Check: Workers Builds: stackflow-docs
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (1)
integrations/react/src/future/useActivityPreparation.ts (1)

4-12: Scalable argument shape looks good

Switching to an object array ({ activityName }[]) makes the hook extensible without breaking changes later. Matches the earlier discussion.

@ENvironmentSet ENvironmentSet merged commit d906597 into main Aug 18, 2025
9 checks passed
@ENvironmentSet ENvironmentSet deleted the route-preload branch August 18, 2025 09:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants